Spring AOP
# AOP 概念
# 什么是 AOP
AOP(Aspect-Oriented Programming,即 面向切面编程)与 OOP( Object-Oriented Programming,面向对象编程) 相辅相成,提供了与 OOP 不同的抽象软件结构的视角。
在 OOP 中,我们以类(class)作为我们的基本单元,而 AOP 中的基本单元是 Aspect(切面)。
AOP 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
# Spring AOP
Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
# Spring AOP 与 AspectJ AOP 的区别?
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。
# AOP的实现方式
此处为语雀内容卡片,点击链接查看:https://www.yuque.com/hanchanmingqi-zjjw3/kb/wkmwtn#r40od
Spring Aop是一种可以减少大量重复代码的一种编程技术,可以设置一个切面,比如说是某个包下面的所有方法,这些方法在执行的时候就会调用我们写的拦截方法,我们可以做一些类似于日志打印等一些操作。
实现AOP有三种方式:静态代理,使用JDK的Proxy类实现动态代理,使用CGLIB实现动态代理。
# 静态代理
就是在代码里面创建一个代理类,实现目标类的接口,目标对象是代理类的成员变量,外界需要执行方法时,调用代理类的方法,代理类的方法内部先执行额外的操作,日志记录等,然后再调用目标类的方法。
public interface IUserDao {
void save();
}
public class UserDao implements IUserDao {
public void save() {
System.out.println("已经保存数据...");
}
}
public class UserDaoProxy implements IUserDao {
private IUserDao target;
public UserDaoProxy(IUserDao iuserDao) {
this.target = iuserDao;
}
public void save() {
System.out.println("开启事务...");
target.save();
System.out.println("关闭事务...");
}
}
# JDK动态代理
通过调用Proxy.newProxyInstance()方法可以为目标类创建一个代理类,然后调用代理类的方法时会调用InvocationHandler的invoke()方法,然后我们可以在invoke()方法里面做一些日志记录之类的额外操作,然后再调用真正的实现方法,也就是目标类的方法。
目标类必须有对应的接口类,我们拦截的方法必须是接口中定义的方法。
public class Test implements TestInterface {
public void test(Integer a) {
System.out.printf("111111");
}
}
public interface TestInterface {
void test(Integer a);
}
public static class CustomInvocationHandler implements InvocationHandler {
Object target;
public CustomInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
Object result = method.invoke(target, args);
System.out.println("after");
return null;
}
}
Test test = new Test();
TestInterface proxyInstance = (TestInterface) Proxy.newProxyInstance(test.getClass().getClassLoader(), test.getClass().getInterfaces(), new CustomInvocationHandler(test));
proxyInstance.test(11);
实现原理:就是在调用Proxy.newProxyInstance()时会根据类加载器和目标类Class对象动态创建一个代理类出来,动态代理类的所有方法的实现都是向下面这样,方法内部都是调用invocationHandler的invoke()方法
public final void test(){
throws
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
}
throw new UndeclaredThrowableException(localThrowable);
}
# CGLIB动态代理
CGLIB主要是通过创建一个代理类,继承原来的类,并且重写父类的方法,然后将代理类实例返回,后续调用代理类的方法时,先执行一些额外的AOP相关的记录操作,然后再去执行父类的方法。(由于在Java中子类只能继承父类的非private的属性和方法,所以**由CGLIB创建的代理类,不会包含父类中的final或者private修饰的方法,**aop也无法捕获private相关的方法)
创建一个类,继承MethodInterceptor类,重写intercept方法,接受方法调用。创建一个Enhancer实例,设置代理类的父类为目标类,设置回调。
public static void main(final String[] args) {
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/ruiwendaier/Downloads/testaop");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test.class);
enhancer.setCallback(new MyMethodInterceptor());
Test test1 = (Test)enhancer.create();
test1.test();
}
public static class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("Before: " + method.getName());
Object object = methodProxy.invokeSuper(o, objects);
System.out.println("After: " + method.getName());
return object;
}
}
public static class Test implements TestInterface {
public void test() {
System.out.printf("111111");
}
}
生成的代理类中,对于父类中每一个能够继承重写的方法,动态代理类都会生成两个相应的方法。一个是方法内是直接调用父类(也就是目标类的方法),一个是生成的对应的动态代理的方法,里面会先调用代理类设置的intercept回调方法,然后再调用父类的方法。在调用时,会直接先调用重写的方法。
final String CGLIB$test$0(){
return super.test();
}
public final String test()
{
MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
if (tmp4_1 == null){
tmp4_1;
CGLIB$BIND_CALLBACKS(this);
}
MethodInterceptor tmp17_14 = this.CGLIB$CALLBACK_0;
if (tmp17_14 != null)
return (String)tmp17_14.intercept(this, CGLIB$test$0$Method, CGLIB$emptyArgs, CGLIB$test$0$Proxy);
return super.test();
}
# 区别
# 静态代理和动态代理的对比
- 灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
# JDK动态代理和CGLIB动态代理比较?
- JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
- Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
- 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
- 目标类必须有对应的接口类,然后JDK动态代理动态创建了一个类,实现了接口中的方法,不能对没有接口类的普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类; Java动态代理只能对有接口类的类进行代理,并且使用Java原生的反射API进行操作,在生成类上比较高效,但是执行会效率低一些。CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效。
# 术语
# Aspect(切面)
aspect
由 pointcut
和 advice
组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义。 Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中. AOP 的工作重心在于如何将增强(advice)织入目标对象的连接点上, 这里包含两个工作:
- 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
- 如何在 advice 中编写切面代码.
可以简单地认为, 使用 @Aspect 注解的类就是切面.
# advice(增强)
由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码。许多 AOP 框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截。例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了.
# 连接点(join point)
a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理。在 Spring AOP 中, **join point 总是方法的执行点, 即只有方法连接点。**
# 切点(point cut)
匹配 join point 的谓词(a predicate that matches join points)。Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行. 在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 **pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.**
# 关于 join point 和 point cut 的区别
在 Spring AOP 中, 所有的方法执行都是 join point。 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice。因此 join point 和 point cut 本质上就是两个不同纬度上的东西. advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice
# introduction
为一个类型添加额外的方法或字段。 Spring AOP 允许我们为 目标对象
引入新的接口(和对应的实现). 例如我们可以使用 introduction 来为一个 bean 实现 IsModified 接口, 并以此来简化 caching 的实现。
# 目标对象(Target)
织入 advice 的目标对象. 目标对象也被称为 advised object
. 因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 **adviced object 总是一个代理对象(proxied object)**
注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.
# AOP proxy
一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类。在 Spring AOP 中,一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象。
# 织入(Weaving)
将 aspect 和其他对象连接起来, 并创建 adviced object 的过程. 根据不同的实现技术, AOP 织入有三种方式:
- 编译器织入, 这要求有特殊的 Java 编译器。
- 类装载期织入, 这需要有特殊的类装载器。
- 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式。
Spring 采用动态代理织入,而 AspectJ 采用编译器织入和类装载期织入。
# advice 的类型
- before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
- after return advice, 在一个 join point 正常返回后执行的 advice
- after throwing advice, 当一个 join point 抛出异常后执行的 advice
- after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
- around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
# 关于 AOP Proxy
Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理. 如果需要为一个类实现代理, 那么可以使用 **CGLIB 代理**.
当一个业务逻辑对象没有实现接口时, 那么 Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类.
# 彻底理解 aspect, join point, point cut, advice
看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的. 下面我以一个简单的例子来比喻一下 AOP 中 aspect, jointpoint, pointcut 与 advice 之间的关系.
让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系. 首先我们知道, 在 Spring AOP 中 join point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, join point 就相当于 爪哇的小县城里的百姓, point cut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问. 为什么可以这样类比呢?
join point --> 爪哇的小县城里的百姓: 因为根据定义, join point 是所有可能被织入 advice 的候选的点, 在 Spring AOP 中, 则可以认为所有方法执行点都是 join point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.
point cut --> 男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 advice, 但是我们并不希望在所有方法上都织入 advice, 而 pointcut 的作用就是提供一组规则来匹配 joinpoint, 给满足规则的 joinpoint 添加 advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据
凶手是个男性, 身高约七尺五寸
, 把符合条件的人抓起来. 在这里凶手是个男性, 身高约七尺五寸
就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.advice --> 抓过来审问, advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的. 同理, 对比到我们的例子中,
抓过来审问
这个动作就是对作用于那些满足男性, 身高约七尺五寸
的爪哇的小县城里的百姓
.aspect: aspect 是 point cut 与 advice 的组合, 因此在这里我们就可以类比: "根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问" 这一整个动作可以被认为是一个 aspect.
或则我们也可以从语法的角度来简单类比一下. 我们在学英语时, 经常会接触什么 定语
, 被动句
之类的概念, 那么可以做一个不严谨的类比, 即 joinpoint
可以认为是一个 宾语
, 而 pointcut
则可以类比为修饰 joinpoint
的定语, 那么整个 aspect
就可以描述为: 满足 pointcut 规则的 joinpoint 会被添加相应的 advice 操作.
# @AspectJ 支持
@AspectJ
是一种使用 Java 注解来实现 AOP 的编码风格。- @AspectJ 风格的 AOP 是 AspectJ Project 在 AspectJ 5 中引入的, 并且 Spring 也支持 @AspectJ 的 AOP 风格.
# 使能 @AspectJ 支持
@AspectJ 可以以 XML 的方式或以注解的方式来使能, 并且不论以哪种方式使能@ASpectJ, 我们都必须保证 aspectjweaver.jar 在 classpath 中.
# 使用 Java Configuration 方式使能@AspectJ
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
# 使用 XML 方式使能@AspectJ
<aop:aspectj-autoproxy/>
# 定义 aspect(切面)
当使用注解 @Aspect 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如:
@Component
@Aspect
public class MyTest {
}
注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 因此我们还需要使用类似 @Component 之类的注解.注意, 如果一个 类被@Aspect 标注, 则这个类就不能是其他 aspect 的 advised object 了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外.
# 声明 pointcut
一个 pointcut 的声明由两部分组成:
- 一个方法签名, 包括方法名和相关参数
- 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice).
在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 即:
@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式
private void dataAccessOperation() {} // 切点前面
这个方法必须无返回值.
这个方法本身就是 pointcut signature, pointcut 表达式使用@Pointcut 注解指定.
上面我们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配所有在包 com.xys.service.UserService 下的所有方法的执行。
# 切点标志符(designator)
AspectJ5 的切点表达式由标志符(designator)和操作参数组成. 如 "execution( greetTo(..))" 的切点表达式, execution 就是 标志符, 而圆括号里的 greetTo(..) 就是操作参数
# execution
匹配 join point 的执行, 例如 "execution(* hello(..))" 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符.
# within
匹配特定包下的所有 join point, 例如 within(com.xys.*)
表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法. 而within(com.xys.service.*Service)
表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点.
# this 与 target
this 的作用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of). 而 target 匹配的是一个目标对象(target object, 即需要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of).
# bean
匹配 bean 名字为指定值的 bean 下的所有方法, 例如:
bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法
bean(myService) // 匹配名字为 myService 的 bean 下的所有方法
# args
匹配参数满足要求的的方法. 例如:
@Pointcut("within(com.xys.demo2.*)")
public void pointcut2() {
}
@Before(value = "pointcut2() && args(name)")
public void doSomething(String name) {
logger.info("---page: {}---", name);
}
@Service
public class NormalService {
private Logger logger = LoggerFactory.getLogger(getClass());
public void someMethod() {
logger.info("---NormalService: someMethod invoked---");
}
public String test(String name) {
logger.info("---NormalService: test invoked---");
return "服务一切正常";
}
}
当 NormalService.test 执行时, 则 advice doSomething
就会执行, test 方法的参数 name 就会传递到 doSomething
中.
常用例子:
// 匹配只有一个参数 name 的方法
@Before(value = "aspectMethod() && args(name)")
public void doSomething(String name) {
}
// 匹配第一个参数为 name 的方法
@Before(value = "aspectMethod() && args(name, ..)")
public void doSomething(String name) {
}
// 匹配第二个参数为 name 的方法
Before(value = "aspectMethod() && args(*, name, ..)")
public void doSomething(String name) {
}
# @annotation
匹配由指定注解所标注的方法, 例如:
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}
则匹配由注解 AuthChecker
所标注的方法.
# 常见的切点表达式
# 匹配方法签名
// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))
// 匹配当前包中的指定类的所有方法
execution(* UserService.*(..))
// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))
// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法
execution(public int com.xys.service.*(..))
// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.xys.service.*(String name, ..))
# 匹配类型签名
// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)
// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)
// 匹配当前包中的指定类中的方法
within(UserService)
// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)
# 匹配 Bean 名字
// 匹配以指定名字结尾的 Bean 中的所有方法
bean(*Service)
# 切点表达式组合
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)
// 匹配名字以 Service 结尾, 并且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)
# 声明 advice
advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围 运行. **pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式**
. 下面我们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的.
# Before advice
/**
* @author xiongyongshun
* @version 1.0
* @created 16/9/9 13:13
*/
@Component
@Aspect
public class BeforeAspectTest {
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
@Pointcut("execution(* com.xys.service.UserService.*(..))")
public void dataAccessOperation() {
}
}
@Component
@Aspect
public class AdviseDefine {
// 定义 advise
@Before("com.xys.aspect.PointcutDefine.dataAccessOperation()")
public void doBeforeAccessCheck(JoinPoint joinPoint) {
System.out.println("*****Before advise, method: " + joinPoint.getSignature().toShortString() + " *****");
}
}
这里, @Before 引用了一个 pointcut, 即 "com.xys.aspect.PointcutDefine.dataAccessOperation()" 是一个 pointcut 的名字。
如果我们在 advice 在内置 pointcut, 则可以:(advice和切点同时定义)
@Component
@Aspect
public class AdviseDefine {
// 将 pointcut 和 advice 同时定义
@Before("within(com.xys.service..*)")
public void doAccessCheck(JoinPoint joinPoint) {
System.out.println("*****doAccessCheck, Before advise, method: " + joinPoint.getSignature().toShortString() + " *****");
}
}
# around advice
around advice 比较特别, 它可以在一个方法的之前之前和之后添加不同的操作, 并且甚至可以决定何时, 如何, 是否调用匹配到的方法.
@Component
@Aspect
public class AdviseDefine {
// 定义 advise
@Around("com.xys.aspect.PointcutDefine.dataAccessOperation()")
public Object doAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 开始
Object retVal = pjp.proceed();
stopWatch.stop();
// 结束
System.out.println("invoke method: " + pjp.getSignature().getName() + ", elapsed time: " + stopWatch.getTotalTimeMillis());
return retVal;
}
}
around advice 和前面的 before advice 差不多, 只是我们把注解 @Before 改为了 @Around 了.